Перейти к основному содержимому

5.15. Модули и организация кода

Разработчику Архитектору

Модули и организация кода

Модули и организация кода
require() — загрузка модулей.
Создание модуля
Пространства имён, избегание глобальных переменных.

По мере роста программной системы возникает необходимость структурирования кода: разделения его на логические компоненты, инкапсуляции функциональности и управления зависимостями. В Lua эта задача решается с помощью механизма модулей, который, хотя и эволюционировал со временем, сохраняет свою минималистичную природу — в полном соответствии с философией языка.

Центральным элементом организации кода в Lua является функция require, предназначенная для однократной загрузки и инициализации модуля по его имени.

local mymodule = require("mymodule")

Принцип работы

  • Lua проверяет, не был ли уже загружен модуль с именем "mymodule" (в таблице package.loaded).
  • Если нет — ищется файл или C-расширение, соответствующее имени, согласно путям, заданным в package.path и package.cpath.
  • Найденный файл выполняется в глобальном окружении, но ожидается, что он вернёт таблицу — интерфейс модуля.
  • Результат кэшируется в package.loaded["mymodule"], чтобы последующие вызовы require возвращали тот же объект без повторного выполнения.

Важно: require гарантирует идемпотентность загрузки — модуль будет выполнен ровно один раз за сессию интерпретатора.

Как ищутся модули?

Lua использует шаблоны путей:

  • package.path — для Lua-файлов (например: ./?.lua;/usr/local/lua/?.lua)
  • package.cpath — для бинарных модулей (C-расширений) Символ ? заменяется на имя модуля. Например:
require("utils")  -- может загрузить ./utils.lua или /usr/share/lua/5.4/utils.lua

Это позволяет реализовать гибкую систему поиска, которую можно расширять динамически.

Модуль в Lua — это любой файл, возвращающий таблицу, которая представляет собой его публичный интерфейс.

Рекомендуемый способ создания модуля:

-- файл: math_utils.lua
local M = {}

function M.square(x)
return x * x
end

function M.cube(x)
return x * x * x
end

-- Приватная функция (не экспортируется)
local function is_positive(x)
return x > 0
end

return M

Загрузка:

local math_utils = require("math_utils")
print(math_utils.square(4)) -- 16

Ранние версии Lua (до 5.2) предлагали функцию module(), автоматически оборачивающую код в глобальный модуль:

module("myoldmodule", package.seeall)
function foo() ... end

Однако этот подход загрязняет глобальное пространство, не поддерживает инкапсуляцию, и удалён в 5.3+. Поэтому всегда используйте явное возвращение таблицы.

Одна из ключевых проблем в Lua — случайное создание глобальных переменных, что может привести к конфликтам и трудноуловимым ошибкам.

function init()
counter = 0 -- ОШИБКА: создана глобальная переменная!
end

Lua по умолчанию разрешает создание глобальных переменных. Это удобно для прототипирования, но неприемлемо в продакшене.

Чтобы обнаружить случайные глобальные присваивания, используйте следующий трюк в начале файла:

-- Блокировка создания новых глобальных переменных
setmetatable(_G, {
__newindex = function(_, name, value)
error("Попытка создать глобальную переменную '" .. name .. "'", 2)
end,
__index = function(_, name)
error("Попытка прочитать несуществующую глобальную переменную '" .. name .. "'", 2)
end
})

Теперь любое обращение к необъявленной глобальной переменной вызовет ошибку — отличный способ повысить надёжность. Альтернатива: использовать строгий режим через сторонние библиотеки (strict.lua) или статические анализаторы (например, luacheck). Мы как раз об этом говорили ранее.

Для логической группировки функций и значений применяйте таблицы:

-- network/http.lua
local http = {}

function http.get(url)
...
end

function http.post(url, data)
...
end

return http

-- main.lua
local http = require("network.http")
http.get("https://example.com")

Таким образом, вы создаёте логическое пространство имён, аналогичное пакетам в Python или модулям в JavaScript.

А как модули организовать? Давайте поговорим об иерархии модулей и организации проекта.

В крупных приложениях модули организуются в древовидную структуру:

project/
├── main.lua
├── utils/
│ ├── string.lua
│ ├── table.lua
│ └── index.lua
├── game/
│ ├── player.lua
│ └── world.lua
└── config.lua

Пример иерархического доступа:

local str_util = require("utils.string")
local player = require("game.player")

Если require("utils") — и в директории utils есть init.lua или index.lua, он будет загружен как содержимое модуля:

-- utils/init.lua
return {
string = require("utils.string"),
table = require("utils.table"),
}

Теперь можно писать:

local utils = require("utils")
utils.string.trim(" hello ")

Это стандартный паттерн для создания сборных модулей. Lua допускает циклические зависимости между модулями, но они могут привести к неожиданному поведению.

-- a.lua
local B = require("b")
local A = { value = "A" }
function A.use_b() return B.value end
return A

-- b.lua
local A = require("a") -- a ещё не завершён!
local B = { value = "B" }
return B

В момент require("a") внутри b.lua, модуль a ещё не вернул таблицу — A будет nil.

Решением будет отложенная загрузка или внедрение зависимостей.

  1. Перенос require внутрь функций:
function B.use_a()
local A = require("a")
return A.value
end
  1. Инъекция зависимостей:
-- Вместо require — передача через параметры
return function(dependencies)
local A = dependencies.A
...
end

Модули в Lua кэшируются — каждый require возвращает один и тот же объект. Это означает, что модули по сути являются синглтонами. Если модуль хранит изменяемое состояние:

-- counter.lua
local count = 0
local M = {}
function M.inc() count = count + 1 end
function M.get() return count end
return M

То все части программы будут делить это состояние. Это может быть полезно (логгер, конфиг), но опасно при неосторожном использовании. По возможности делайте модули stateless (без состояния), а состояние передавайте явно.

Lua предоставляет минимальный, но достаточный набор средств. Его гибкость позволяет строить сложные системы, но ответственность за порядок лежит на разработчике.